home *** CD-ROM | disk | FTP | other *** search
/ Chip 2006 June / CHIP 2006-06.2.iso / program / freeware / Democracy-0.8.2.exe / xulrunner / python / app.py < prev    next >
Encoding:
Python Source  |  2006-04-10  |  65.0 KB  |  1,839 lines

  1. import util
  2. import feed
  3. import item
  4. import config       # IMPORTANT!! config MUST be imported before downloader
  5. import folder
  6. import autodler
  7. import resource
  8. import template
  9. import database
  10. import scheduler
  11. import downloader
  12. import autoupdate
  13. import xhtmltools
  14. import guide
  15. import idlenotifier
  16.  
  17. import os
  18. import re
  19. import sys
  20. import cgi
  21. import copy
  22. import time
  23. import types
  24. import random
  25. import datetime
  26. import traceback
  27. import datetime
  28. import threading
  29.  
  30. from xml.dom.minidom import parse, parseString
  31.  
  32. # Something needs to import this outside of Pyrex. Might as well be app
  33. import templatehelper
  34. import databasehelper
  35. import fasttypes
  36.  
  37. from dl_daemon import daemon
  38.  
  39. db = database.defaultDatabase
  40.  
  41. # Run the application. Call this, not start(), on platforms where we
  42. # are responsible for the event loop.
  43. def main():
  44.     Controller().Run()
  45.  
  46. # Start up the application and return. Call this, not main(), on
  47. # platform where we are not responsible for the event loop.
  48. def start():
  49.     Controller().runNonblocking()
  50.  
  51.  
  52. ###############################################################################
  53. #### The Playback Controller base class                                    ####
  54. ###############################################################################
  55.  
  56. class PlaybackControllerBase:
  57.     
  58.     def __init__(self):
  59.         self.currentPlaylist = None
  60.         self.currentDisplay = None
  61.  
  62.     def configure(self, view, firstItemId=None):
  63.         self.currentPlaylist = Playlist(view, firstItemId)
  64.     
  65.     def reset(self):
  66.         if self.currentPlaylist is not None:
  67.             self.currentPlaylist.reset()
  68.             self.currentPlaylist = None
  69.         self.currentDisplay = None
  70.     
  71.     def enterPlayback(self):
  72.         if self.currentPlaylist is not None:
  73.             startItem = self.currentPlaylist.cur()
  74.             if startItem is not None:
  75.                 self.playItem(startItem)
  76.         
  77.     def exitPlayback(self, switchDisplay=True):
  78.         self.reset()
  79.         if switchDisplay:
  80.             Controller.instance.displayCurrentTabContent()
  81.     
  82.     def playPause(self):
  83.         videoDisplay = Controller.instance.videoDisplay
  84.         if self.currentDisplay == videoDisplay:
  85.             videoDisplay.playPause()
  86.         else:
  87.             self.enterPlayback()
  88.  
  89.     def playItem(self, anItem):
  90.         try:
  91.             self.skipIfItemFileIsMissing(anItem)
  92.             videoDisplay = Controller.instance.videoDisplay
  93.             if videoDisplay.canPlayItem(anItem):
  94.                 self.playItemInternally(videoDisplay, anItem)
  95.             else:
  96.                 if self.currentDisplay is videoDisplay:
  97.                     if videoDisplay.isFullScreen:
  98.                         videoDisplay.exitFullScreen()
  99.                     videoDisplay.stop()
  100.                 self.scheduleExternalPlayback(anItem)
  101.         except:
  102.             util.failedExn('when trying to play a video')
  103.             self.stop()
  104.  
  105.     def playItemInternally(self, videoDisplay, anItem):
  106.         if self.currentDisplay is not videoDisplay:
  107.             self.currentDisplay = videoDisplay
  108.             frame = Controller.instance.frame
  109.             frame.selectDisplay(videoDisplay, frame.mainDisplay)
  110.         videoDisplay.selectItem(anItem)
  111.         videoDisplay.play()
  112.  
  113.     def playItemExternally(self, itemID):
  114.         anItem = mapToPlaylistItem(db.getObjectByID(int(itemID)))
  115.         self.currentDisplay = TemplateDisplay('external-playback-continue', anItem.getInfoMap(), Controller.instance)
  116.         frame = Controller.instance.frame
  117.         frame.selectDisplay(self.currentDisplay, frame.mainDisplay)
  118.         return anItem
  119.         
  120.     def scheduleExternalPlayback(self, anItem):
  121.         Controller.instance.videoDisplay.stopOnDeselect = False
  122.         self.currentDisplay = TemplateDisplay('external-playback', anItem.getInfoMap(), Controller.instance)
  123.         frame = Controller.instance.frame
  124.         frame.selectDisplay(self.currentDisplay, frame.mainDisplay)
  125.  
  126.     def stop(self, switchDisplay=True):
  127.         videoDisplay = Controller.instance.videoDisplay
  128.         if self.currentDisplay == videoDisplay:
  129.             videoDisplay.stop()
  130.         self.exitPlayback(switchDisplay)
  131.  
  132.     def skip(self, direction):
  133.         nextItem = None
  134.         if self.currentPlaylist is not None:
  135.             if direction == 1:
  136.                 nextItem = self.currentPlaylist.getNext()
  137.             else:
  138.                 if not hasattr(self.currentDisplay, 'getCurrentTime') or self.currentDisplay.getCurrentTime() <= 1.0:
  139.                     nextItem = self.currentPlaylist.getPrev()
  140.                 else:
  141.                     self.currentDisplay.goToBeginningOfMovie()
  142.                     return self.currentPlaylist.cur()
  143.         if nextItem is None:
  144.             self.stop()
  145.         else:
  146.             self.playItem(nextItem)
  147.         return nextItem
  148.  
  149.     def skipIfItemFileIsMissing(self, anItem):
  150.         path = anItem.getPath()
  151.         if not os.path.exists(path):
  152.             print "DTV: movie file '%s' is missing, skipping to next" % path
  153.             self.onMovieFinished()
  154.  
  155.     def onMovieFinished(self):
  156.         if self.skip(1) is None:
  157.             self.stop()
  158.  
  159.  
  160. ###############################################################################
  161. #### Base class for displays                                               ####
  162. #### This must be defined before we import the frontend                    ####
  163. ###############################################################################
  164.  
  165. class Display:
  166.     "Base class representing a display in a MainFrame's right-hand pane."
  167.  
  168.     def __init__(self):
  169.         self.currentFrame = None # tracks the frame that currently has us selected
  170.  
  171.     def isSelected(self):
  172.         return self.currentFrame is not None
  173.  
  174.     def onSelected(self, frame):
  175.         "Called when the Display is shown in the given MainFrame."
  176.         pass
  177.  
  178.     def onDeselected(self, frame):
  179.         """Called when the Display is no longer shown in the given
  180.         MainFrame. This function is called on the Display losing the
  181.         selection before onSelected is called on the Display gaining the
  182.         selection."""
  183.         pass
  184.  
  185.     def onSelected_private(self, frame):
  186.         assert(self.currentFrame == None)
  187.         self.currentFrame = frame
  188.  
  189.     def onDeselected_private(self, frame):
  190.         assert(self.currentFrame == frame)
  191.         self.currentFrame = None
  192.  
  193.     # The MainFrame wants to know if we're ready to display (eg, if the
  194.     # a HTML display has finished loading its contents, so it can display
  195.     # immediately without flicker.) We're to call hook() when we're ready
  196.     # to be displayed.
  197.     def callWhenReadyToDisplay(self, hook):
  198.         hook()
  199.  
  200.     def cancel(self):
  201.         """Called when the Display is not shown because it is not ready yet
  202.         and another display will take its place"""
  203.         pass
  204.  
  205.     def getWatchable(self):
  206.         """Subclasses can implement this if they can return a database view
  207.         of watchable items"""
  208.         return None
  209.  
  210.  
  211. ###############################################################################
  212. #### Provides cross platform part of Video Display                         ####
  213. #### This must be defined before we import the frontend                    ####
  214. ###############################################################################
  215.  
  216. class VideoDisplayBase (Display):
  217.     
  218.     def __init__(self):
  219.         Display.__init__(self)
  220.         self.playbackController = None
  221.         self.volume = 1.0
  222.         self.previousVolume = 1.0
  223.         self.isPlaying = False
  224.         self.isFullScreen = False
  225.         self.stopOnDeselect = True
  226.         self.renderers = list()
  227.         self.activeRenderer = None
  228.  
  229.     def initRenderers(self):
  230.         pass
  231.         
  232.     def getRendererForItem(self, anItem):
  233.         for renderer in self.renderers:
  234.             if renderer.canPlayItem(anItem):
  235.                 return renderer
  236.         return None
  237.  
  238.     def canPlayItem(self, anItem):
  239.         return self.getRendererForItem(anItem) is not None
  240.     
  241.     def selectItem(self, anItem):
  242.         self.stopOnDeselect = True
  243.         
  244.         info = anItem.getInfoMap()
  245.         template = TemplateDisplay('video-info', info, Controller.instance, None, None, None)
  246.         area = Controller.instance.frame.videoInfoDisplay
  247.         Controller.instance.frame.selectDisplay(template, area)
  248.         
  249.         self.activeRenderer = self.getRendererForItem(anItem)
  250.         self.activeRenderer.selectItem(anItem)
  251.         self.activeRenderer.setVolume(self.getVolume())
  252.  
  253.     def reset(self):
  254.         self.isPlaying = False
  255.         self.stopOnDeselect = True
  256.         if self.activeRenderer is not None:
  257.             self.activeRenderer.reset()
  258.         self.activeRenderer = None
  259.  
  260.     def goToBeginningOfMovie(self):
  261.         if self.activeRenderer is not None:
  262.             self.activeRenderer.goToBeginningOfMovie()
  263.  
  264.     def playPause(self):
  265.         if self.isPlaying:
  266.             self.pause()
  267.         else:
  268.             self.play()
  269.  
  270.     def play(self):
  271.         if self.activeRenderer is not None:
  272.             self.activeRenderer.play()
  273.         self.isPlaying = True
  274.  
  275.     def pause(self):
  276.         if self.activeRenderer is not None:
  277.             self.activeRenderer.pause()
  278.         self.isPlaying = False
  279.  
  280.     def stop(self):
  281.         if self.isFullScreen:
  282.             self.exitFullScreen()
  283.         if self.activeRenderer is not None:
  284.             self.activeRenderer.stop()
  285.         self.reset()
  286.  
  287.     def goFullScreen(self):
  288.         self.isFullScreen = True
  289.         if not self.isPlaying:
  290.             self.play()
  291.  
  292.     def exitFullScreen(self):
  293.         self.isFullScreen = False
  294.  
  295.     def getCurrentTime(self):
  296.         if self.activeRenderer is not None:
  297.             return self.activeRenderer.getCurrentTime()
  298.         return VideoRenderer.DEFAULT_DISPLAY_TIME
  299.  
  300.     def setVolume(self, level):
  301.         self.volume = level
  302.         config.set(config.VOLUME_LEVEL, level)
  303.         if self.activeRenderer is not None:
  304.             self.activeRenderer.setVolume(level)
  305.  
  306.     def getVolume(self):
  307.         return self.volume
  308.  
  309.     def muteVolume(self):
  310.         self.previousVolume = self.getVolume()
  311.         self.setVolume(0.0)
  312.  
  313.     def restoreVolume(self):
  314.         self.setVolume(self.previousVolume)
  315.  
  316.     def onDeselected(self, frame):
  317.         if self.isPlaying and self.stopOnDeselect:
  318.             Controller.instance.playbackController.stop(False)
  319.  
  320.     
  321. ###############################################################################
  322. #### Video renderer base class                                             ####
  323. ###############################################################################
  324.  
  325. class VideoRenderer:
  326.     
  327.     DISPLAY_TIME_FORMAT  = "%H:%M:%S"
  328.     DEFAULT_DISPLAY_TIME = time.strftime(DISPLAY_TIME_FORMAT, time.gmtime(0))
  329.     
  330.     def __init__(self):
  331.         self.interactivelySeeking = False
  332.     
  333.     def canPlayItem(self, anItem):
  334.         return False
  335.     
  336.     def getDisplayTime(self):
  337.         seconds = self.getCurrentTime()
  338.         return time.strftime(self.DISPLAY_TIME_FORMAT, time.gmtime(seconds))
  339.  
  340.     def getProgress(self):
  341.         duration = self.getDuration()
  342.         if duration == 0:
  343.             return 0.0
  344.         return self.getCurrentTime() / duration
  345.  
  346.     def setProgress(self, progress):
  347.         self.setCurrentTime(self.getDuration() * progress)
  348.     
  349.     def selectItem(self, anItem):
  350.         pass
  351.         
  352.     def reset(self):
  353.         pass
  354.  
  355.     def getCurrentTime(self):
  356.         return 0.0
  357.  
  358.     def setCurrentTime(self, seconds):
  359.         pass
  360.  
  361.     def getDuration(self):
  362.         return 0.0
  363.  
  364.     def setVolume(self, level):
  365.         pass
  366.                 
  367.     def goToBeginningOfMovie(self):
  368.         pass
  369.         
  370.     def play(self):
  371.         pass
  372.         
  373.     def pause(self):
  374.         pass
  375.         
  376.     def stop(self):
  377.         pass
  378.     
  379.     def getRate(self):
  380.         return 1.0
  381.     
  382.     def setRate(self, rate):
  383.         pass
  384.         
  385.         
  386. # We can now safely import the frontend module
  387. import frontend
  388.  
  389. ###############################################################################
  390. #### The main application controller object, binding model to view         ####
  391. ###############################################################################
  392.  
  393. class Controller (frontend.Application):
  394.  
  395.     # This is considered public for now (ugly, sorry)
  396.     instance = None
  397.  
  398.     def __init__(self):
  399.         frontend.Application.__init__(self)
  400.         assert Controller.instance is None
  401.         Controller.instance = self
  402.  
  403.     ### Startup and shutdown ###
  404.  
  405.     def onStartup(self):
  406.         try:
  407.             print "DTV: Loading preferences..."
  408.             config.load()
  409.             config.addChangeCallback(self.configDidChange)
  410.             
  411.             delegate = self.getBackendDelegate()
  412.             feed.setDelegate(delegate)
  413.             feed.setSortFunc(itemSort)
  414.             downloader.setDelegate(delegate)
  415.             autoupdate.setDelegate(delegate)
  416.             database.setDelegate(delegate)
  417.  
  418.             downloader.RemoteDownloader.initializeDaemon()
  419.  
  420.             #Restoring
  421.             print "DTV: Restoring database..."
  422.             db.restore()
  423.             print "DTV: Recomputing filters..."
  424.             db.recomputeFilters()
  425.  
  426.             # If there's no Channel Guide in the database, create one
  427.             # and some test feeds
  428.             hasGuide = False
  429.             for obj in globalViewList['guide']:
  430.                 hasGuide = True
  431.                 channelGuide = obj
  432.             if not hasGuide:
  433.                 print "DTV: Spawning Channel Guide..."
  434.                 channelGuide = guide.ChannelGuide()
  435.                 feed.Feed('http://del.icio.us/rss/representordie/system:media:video', initiallyAutoDownloadable=False)
  436.                 feed.Feed('http://www.videobomb.com/rss/posts/front', initiallyAutoDownloadable=False)
  437.                 feed.Feed('http://www.mediarights.org/bm/rss.php?i=1', initiallyAutoDownloadable=False)
  438.                 feed.Feed('http://www.telemusicvision.com/videos/rss.php?i=1', initiallyAutoDownloadable=False)
  439.                 feed.Feed('http://www.rocketboom.com/vlog/quicktime_daily_enclosures.xml', initiallyAutoDownloadable=False)
  440.                 feed.Feed('http://www.channelfrederator.com/rss', initiallyAutoDownloadable=False)
  441.                 feed.Feed('http://revision3.com/diggnation/feed/small.mov', initiallyAutoDownloadable=False)
  442.                 feed.Feed('http://live.watchmactv.com/wp-rss2.php', initiallyAutoDownloadable=False)
  443.                 feed.Feed('http://some-pig.net/videos/rss.php?i=2', initiallyAutoDownloadable=False)
  444.  
  445.             # Define variables for templates
  446.             # NEEDS: reorganize this, and update templates
  447.             globalData = {
  448.                 'database': db,
  449.                 'filter': globalFilterList,
  450.                 'sort': globalSortList,
  451.                 'view': globalViewList,
  452.                 'index': globalIndexList,
  453.                 'guide': channelGuide,
  454.                 }
  455.             tabPaneData = {
  456.                 'global': globalData,
  457.                 }
  458.  
  459.             globalData['view']['availableItems'].addAddCallback(self.onAvailableItemsCountChange)
  460.             globalData['view']['availableItems'].addRemoveCallback(self.onAvailableItemsCountChange)
  461.             globalData['view']['downloadingItems'].addAddCallback(self.onDownloadingItemsCountChange)
  462.             globalData['view']['downloadingItems'].addRemoveCallback(self.onDownloadingItemsCountChange)
  463.  
  464.             # Set up the search objects
  465.             self.setupGlobalFeed('dtv:search')
  466.             self.setupGlobalFeed('dtv:searchDownloads')
  467.  
  468.             # Set up tab list
  469.             reloadStaticTabs()
  470.             mapFunc = makeMapToTabFunction(globalData, self)
  471.             self.tabs = db.filter(mappableToTab).map(mapFunc).sort(sortTabs)
  472.  
  473.             self.tabIDIndex = lambda x: x.id
  474.             self.tabs.createIndex(self.tabIDIndex)
  475.  
  476.             self.tabObjIDIndex = lambda x: x.obj.getID()
  477.             self.tabs.createIndex(self.tabObjIDIndex)
  478.  
  479.             self.currentSelectedTab = None
  480.             self.tabListActive = True
  481.             tabPaneData['tabs'] = self.tabs
  482.  
  483.             # Keep a ref of the 'new' and 'download' tabs, we'll need'em later
  484.             self.newTab = None
  485.             self.downloadTab = None
  486.             for tab in self.tabs:
  487.                 if tab.tabTemplateBase == 'newtab':
  488.                     self.newTab = tab
  489.                 elif tab.tabTemplateBase == 'downloadtab':
  490.                     self.downloadTab = tab
  491.  
  492.             # Put cursor on first tab to indicate that it should be initially
  493.             # selected
  494.             self.tabs.resetCursor()
  495.             self.tabs.getNext()
  496.  
  497.             # If we're missing the file system videos feed, create it
  498.             self.setupGlobalFeed('dtv:directoryfeed')
  499.  
  500.             # Start the automatic downloader daemon
  501.             print "DTV: Spawning auto downloader..."
  502.             autodler.AutoDownloader()
  503.  
  504.             # Start the idle notifier daemon
  505.             if config.get(config.LIMIT_UPSTREAM) is True:
  506.                 print "DTV: Spawning idle notifier"
  507.                 self.idlingNotifier = idlenotifier.IdleNotifier(self)
  508.                 self.idlingNotifier.start()
  509.             else:
  510.                 self.idlingNotifier = None
  511.  
  512.             # Set up the playback controller
  513.             self.playbackController = frontend.PlaybackController()
  514.  
  515.             # Put up the main frame
  516.             print "DTV: Displaying main frame..."
  517.             self.frame = frontend.MainFrame(self)
  518.  
  519.             # Set up the video display
  520.             self.videoDisplay = frontend.VideoDisplay()
  521.             self.videoDisplay.initRenderers()
  522.             self.videoDisplay.playbackController = self.playbackController
  523.             self.videoDisplay.setVolume(config.get(config.VOLUME_LEVEL))
  524.  
  525.             scheduler.ScheduleEvent(300,db.save)
  526.  
  527.             scheduler.ScheduleEvent(10, autoupdate.checkForUpdates, False)
  528.             scheduler.ScheduleEvent(86400, autoupdate.checkForUpdates)
  529.  
  530.             # Set up tab list (on left); this will automatically set up the
  531.             # display area (on right) and currentSelectedTab
  532.             self.tabDisplay = TemplateDisplay('tablist', tabPaneData, self)
  533.             self.frame.selectDisplay(self.tabDisplay, self.frame.channelsDisplay)
  534.             self.tabs.addRemoveCallback(lambda oldObject, oldIndex: self.checkSelectedTab())
  535.             self.checkSelectedTab()
  536.  
  537.             # If we have newly available items, provide feedback
  538.             self.updateAvailableItemsCountFeedback()
  539.  
  540.             # NEEDS: our strategy above with addRemoveCallback doesn't
  541.             # work. I'm not sure why, but it seems to have to do with the
  542.             # reentrant call back into the database when checkSelectedTab ends 
  543.             # up calling endChange to force a tab to get rerendered.
  544.  
  545.         except:
  546.             util.failedExn("while starting up")
  547.             frontend.exit(1)
  548.  
  549.     def setupGlobalFeed(self, url):
  550.         feedView = globalViewList['feeds'].filterWithIndex(globalIndexList['feedsByURL'], url)
  551.         hasFeed = feedView.len() > 0
  552.         globalViewList['feeds'].removeView(feedView)
  553.         if not hasFeed:
  554.             print "DTV: Spawning global feed %s" % url
  555.             d = feed.Feed(url)
  556.  
  557.     def getGlobalFeed(self, url):
  558.         feedView = globalViewList['feeds'].filterWithIndex(globalIndexList['feedsByURL'], url)
  559.         feedView.resetCursor()
  560.         feed = feedView.getNext()
  561.         globalViewList['feeds'].removeView(feedView)
  562.         return feed
  563.  
  564.     def removeGlobalFeed(self, url):
  565.         feedView = globalViewList['feeds'].filterWithIndex(globalIndexList['feedsByURL'], url)
  566.         feedView.resetCursor()
  567.         feed = feedView.getNext()
  568.         globalViewList['feeds'].removeView(feedView)
  569.         if feed is not None:
  570.             print "DTV: Removing global feed %s" % url
  571.             feed.remove()
  572.  
  573.     # Change the currently selected tab to the one remaining when
  574.     # filtered by index and id. Returns the currently selected tab.
  575.     def checkTabUsingIndex(self, index, id):
  576.         # view should contain only one tab object
  577.         view = self.tabs.filterWithIndex(index, id)
  578.         view.beginUpdate()
  579.         try:
  580.             view.resetCursor()
  581.             obj = view.getNext()
  582.  
  583.         #FIXME: This is a hack.
  584.         # It takes the cursor into view and makes a cursor into self.tabs
  585.         #
  586.         # view.objects[view.cursor][0] is the object in view before it was
  587.         # mapped into a tab
  588.         # 
  589.         # objectLocs is an internal dictionary of object IDs to cursors
  590.         #
  591.         # We need to change the database API to allow this to happen
  592.         # cleanly and give sane error messages (See #1053 and #1155)
  593.             self.tabs.cursor = self.tabs.objectLocs[view.objects[view.cursor][0].getID()]
  594.         finally:
  595.             view.endUpdate()
  596.             self.tabs.removeView(view)
  597.         return obj
  598.  
  599.     # Select a tab given a tab id (as opposed to an object id)
  600.     # Returns the selected tab
  601.     def checkTabByID(self, id):
  602.         return self.checkTabUsingIndex(self.tabIDIndex, id)
  603.  
  604.     # Select a tab given an object id (as opposed to an object id)
  605.     # Returns the selected tab
  606.     def checkTabByObjID(self, id):
  607.         return self.checkTabUsingIndex(self.tabObjIDIndex, id)
  608.  
  609.     def allowShutdown(self):
  610.         allow = True
  611.         downloadsCount = globalViewList['downloadingItems'].len()
  612.         if downloadsCount > 0:
  613.             allow = self.getBackendDelegate().interruptDownloadsAtShutdown(downloadsCount)
  614.         return allow
  615.  
  616.     def onShutdown(self):
  617.         try:
  618.             print "DTV: Saving preferences..."
  619.             config.save()
  620.  
  621.             print "DTV: Stopping scheduler"
  622.             scheduler.ScheduleEvent.scheduler.shutdown()
  623.  
  624.             print "DTV: Removing search feed"
  625.             TemplateActionHandler(self, None, None).resetSearch()
  626.             self.removeGlobalFeed('dtv:search')
  627.  
  628.             print "DTV: Removing static tabs..."
  629.             removeStaticTabs()
  630.             # for item in db:
  631.             #    print str(item.__class__.__name__) + " of id "+str(item.getID())
  632.             print "DTV: Saving database..."
  633.             db.save()
  634.  
  635.             # FIXME closing BitTorrent is slow and makes the application seem hung...
  636.             print "DTV: Shutting down Downloader..."
  637.             downloader.shutdownDownloader()
  638.  
  639.             if self.idlingNotifier is not None:
  640.                 print "DTV: Shutting down IdleNotifier"
  641.                 self.idlingNotifier.join()
  642.  
  643.             print "DTV: Done shutting down."
  644.             print "Remaining threads are:"
  645.             for thread in threading.enumerate():
  646.                 print thread
  647.  
  648.         except:
  649.             util.failedExn("while shutting down")
  650.             frontend.exit(1)
  651.  
  652.     ### Handling config/prefs changes
  653.     
  654.     def configDidChange(self, key, value):
  655.         if key is config.LIMIT_UPSTREAM.key:
  656.             if value is False:
  657.                 # The Windows version can get here without creating an
  658.                 # idlingNotifier
  659.                 try:
  660.                     self.idlingNotifier.join()
  661.                 except:
  662.                     pass
  663.                 self.idlingNotifier = None
  664.             elif self.idlingNotifier is None:
  665.                 self.idlingNotifier = idlenotifier.IdleNotifier(self)
  666.                 self.idlingNotifier.start()
  667.  
  668.     ### Handling system idle events
  669.     
  670.     def systemHasBeenIdlingSince(self, seconds):
  671.         self.setUpstreamLimit(False)
  672.  
  673.     def systemIsActiveAgain(self):
  674.         self.setUpstreamLimit(True)
  675.  
  676.     ### Handling events received from the OS (via our base class) ###
  677.  
  678.     # Called by Frontend via Application base class in response to OS request.
  679.     def addAndSelectFeed(self, url, showTemplate = None):
  680.         return GUIActionHandler(self).addFeed(url, showTemplate)
  681.  
  682.     def addFeedFromFile(self,file):
  683.         feed.addFeedFromFile(file)
  684.         return False
  685.  
  686.     ### Handling 'DTVAPI' events from the channel guide ###
  687.  
  688.     def addFeed(self, url):
  689.         return GUIActionHandler(self).addFeed(url, selected = None)
  690.  
  691.     def selectFeed(self, url):
  692.         return GUIActionHandler(self).selectFeed(url)
  693.  
  694.     ### Keeping track of the selected tab and showing the right template ###
  695.  
  696.     def getTabState(self, tabId):
  697.         # Determine if this tab is selected
  698.         isSelected = False
  699.         if self.currentSelectedTab:
  700.             isSelected = (self.currentSelectedTab.id == tabId)
  701.  
  702.         # Compute status string
  703.         if isSelected:
  704.             if self.tabListActive:
  705.                 return 'selected'
  706.             else:
  707.                 return 'selected-inactive'
  708.         else:
  709.             return 'normal'
  710.  
  711.     def checkSelectedTab(self, templateNameHint = None):
  712.         # NEEDS: locking ...
  713.         # NEEDS: ensure is reentrant (as in two threads calling it simultaneously by accident)
  714.  
  715.         # We'd like to track the currently selected tab entirely with
  716.         # the cursor on self.tabs. Alas, it is not to be -- when
  717.         # getTabState is called from the database code in response to
  718.         # a change to a tab object (say), the cursor has been
  719.         # temporarily moved by the database code. Long-term, we should
  720.         # make the database code not do this. But short-term, we track
  721.         # the the currently selected tab separately too, synchronizing
  722.         # it to the cursor here. This isn't really wasted effort,
  723.         # because this variable is also the mechanism by which we
  724.         # check to see if the cursor has moved since the last call to
  725.         # checkSelectedTab.
  726.         #
  727.         # Why use the cursor at all? It's necessary because we want
  728.         # the database code to handle moving the cursor on a deleted
  729.         # record automatically for us.
  730.  
  731.         oldSelected = self.currentSelectedTab
  732.         newSelected = self.tabs.cur()
  733.         self.currentSelectedTab = newSelected
  734.  
  735.         tabChanged = ((oldSelected == None) != (newSelected == None)) or (oldSelected and newSelected and oldSelected.id != newSelected.id)
  736.         if tabChanged: # Tab selection has changed! Deal.
  737.             # Redraw the old and new tabs
  738.             if oldSelected:
  739.                 oldSelected.redraw()
  740.             if newSelected:
  741.                 newSelected.redraw()
  742.             # Boot up the new tab's template.
  743.             self.displayCurrentTabContent(templateNameHint)
  744.  
  745.     def displayCurrentTabContent(self, templateNameHint = None):
  746.         if self.currentSelectedTab is not None:
  747.             self.currentSelectedTab.start(self.frame, templateNameHint)
  748.         else:
  749.             # If we're in the middle of a shutdown, selectDisplay
  750.             # might not be there... I'm not sure why...
  751.             if hasattr(self,'selectDisplay'):
  752.                 self.selectDisplay(NullDisplay())
  753.  
  754.     def setTabListActive(self, active):
  755.         """If active is true, show the tab list normally. If active is
  756.         false, show the tab list a different way to indicate that it
  757.         doesn't pertain directly to what is going on (for example, a
  758.         video is playing) but that it can still be clicked on."""
  759.         self.tabListActive = active
  760.         if self.tabs.cur():
  761.             self.tabs.cur().redraw()
  762.  
  763.     ### Keep track of currently available+downloading items and refresh the
  764.     ### corresponding tabs accordingly.
  765.  
  766.     def onAvailableItemsCountChange(self, obj, id):
  767.         assert self.newTab is not None
  768.         self.newTab.redraw()
  769.         self.updateAvailableItemsCountFeedback()
  770.  
  771.     def onDownloadingItemsCountChange(self, obj, id):
  772.         assert self.downloadTab is not None
  773.         self.downloadTab.redraw()
  774.  
  775.     def updateAvailableItemsCountFeedback(self):
  776.         count = globalViewList['availableItems'].len()
  777.         self.getBackendDelegate().updateAvailableItemsCountFeedback(count)
  778.  
  779.     ### ----
  780.  
  781.     def onDisplaySwitch(self, newDisplay):
  782.         # Nick, your turn ;)
  783.         pass
  784.         
  785.     def setUpstreamLimit(self, setLimit):
  786.         if setLimit:
  787.             limit = config.get(config.UPSTREAM_LIMIT_IN_KBS)
  788.             # upstream limit should be set here
  789.         else:
  790.             # upstream limit should be unset here
  791.             pass
  792.  
  793.  
  794. ###############################################################################
  795. #### TemplateDisplay: a HTML-template-driven right-hand display panel      ####
  796. ###############################################################################
  797.  
  798. class TemplateDisplay(frontend.HTMLDisplay):
  799.  
  800.     def __init__(self, templateName, data, controller, existingView = None, frameHint=None, areaHint=None, baseURL=None):
  801.         "'templateName' is the name of the inital template file. 'data' is keys for the template."
  802.  
  803.         # Copy the event cookie for this instance (allocated by our
  804.         # base class) into the template data
  805.         data = copy.copy(data)
  806.         data['eventCookie'] = self.getEventCookie()
  807.         data['dtvPlatform'] = self.getDTVPlatformName()
  808.  
  809.         #print "Processing %s" % templateName
  810.         self.controller = controller
  811.         self.templateName = templateName
  812.         self.templateData = data
  813.         (tch, self.templateHandle) = template.fillTemplate(templateName, data, self)
  814.         html = tch.getOutput()
  815.  
  816.         self.actionHandlers = [
  817.             ModelActionHandler(self.controller.getBackendDelegate()),
  818.             GUIActionHandler(self.controller),
  819.             TemplateActionHandler(self.controller, self, self.templateHandle),
  820.             ]
  821.  
  822.         loadTriggers = self.templateHandle.getTriggerActionURLsOnLoad()
  823.         newPage = self.runActionURLs(loadTriggers)
  824.  
  825.         if newPage:
  826.             self.templateHandle.unlinkTemplate()
  827.             self.__init__(re.compile(r"^template:(.*)$").match(url).group(1),data,controller, existingView, frameHint, areaHint, baseURL)
  828.         else:
  829.             frontend.HTMLDisplay.__init__(self, html, existingView=existingView, frameHint=frameHint, areaHint=areaHint, baseURL=baseURL)
  830.  
  831.             thread = threading.Thread(target=self.templateHandle.initialFillIn,\
  832.                                       name="Initial fillin for template %s" %\
  833.                                       templateName)
  834.             thread.setDaemon(False)
  835.             thread.start()
  836.  
  837.     def runActionURLs(self, triggers):
  838.         newPage = False
  839.         for url in triggers:
  840.             if url.startswith('action:'):
  841.                 self.onURLLoad(url)
  842.             elif url.startswith('javascript:'):
  843.                 js = url.replace('javascript:', '')
  844.                 self.execJS(js)
  845.             elif url.startswith('template:'):
  846.                 newPage = True
  847.                 break
  848.         return newPage
  849.         
  850.     def onURLLoad(self, url):
  851.         #print "DTV: got %s" % url
  852.         try:
  853.             # Special-case non-'action:'-format URL
  854.             match = re.compile(r"^template:(.*)$").match(url)
  855.             if match:
  856.                 self.dispatchAction('switchTemplate', name = match.group(1))
  857.                 return False
  858.  
  859.             # Standard 'action:' URL
  860.             match = re.compile(r"^action:([^?]+)(\?(.*))?$").match(url)
  861.             if match:
  862.                 action = match.group(1)
  863.                 argString = match.group(3)
  864.                 if argString is None:
  865.                     argString = ''
  866.                 argLists = cgi.parse_qs(argString, keep_blank_values=True)
  867.  
  868.                 # argLists is a dictionary from parameter names to a list
  869.                 # of values given for that parameter. Take just one value
  870.                 # for each parameter, raising an error if more than one
  871.                 # was given.
  872.                 args = {}
  873.                 for key in argLists.keys():
  874.                     value = argLists[key]
  875.                     if len(value) != 1:
  876.                         raise template.TemplateError, "Multiple values of '%s' argument passed to '%s' action" % (key, action)
  877.             if type(key) == unicode:
  878.             key = key.encode('utf8')
  879.                     args[key] = value[0]
  880.  
  881.                 if self.dispatchAction(action, **args):
  882.                     return False
  883.                 else:
  884.                     print "Ignored bad action URL: %s" % url
  885.                     return False
  886.  
  887.             #NEEDS: handle feed:// URLs and USM subscription URLs
  888.  
  889.             # Let channel guide URLs pass through
  890.             if url.startswith(config.get(config.CHANNEL_GUIDE_URL)):
  891.                 return True
  892.             if url.startswith('file://'):
  893.                 return True
  894.  
  895.             # If we get here, this isn't a DTV URL. We should open it
  896.             # in an external browser.
  897.             if (url.startswith('http://') or url.startswith('https://') or
  898.                 url.startswith('ftp://') or url.startswith('mailto:')):
  899.                 self.controller.getBackendDelegate().openExternalURL(url)
  900.                 return False
  901.  
  902.         except:
  903.             details = "Handling action URL '%s'" % (url, )
  904.             util.failedExn("while handling a request", details = details)
  905.  
  906.         return True
  907.  
  908.     def dispatchAction(self, action, **kwargs):
  909.         for handler in self.actionHandlers:
  910.             if hasattr(handler, action):
  911.                 getattr(handler, action)(**kwargs)
  912.                 return True
  913.  
  914.         return False
  915.  
  916.     def onDeselected(self, frame):
  917.         unloadTriggers = self.templateHandle.getTriggerActionURLsOnUnload()
  918.         self.runActionURLs(unloadTriggers)
  919.         self.templateHandle.unlinkTemplate()
  920.         frontend.HTMLDisplay.onDeselected(self, frame)
  921.  
  922.     def getWatchable(self):
  923.         view = None
  924.         for name in ('watchable-items', 'unwatched-items', 'expiring-items', 'saved-items'):
  925.             try:
  926.                 namedView = self.templateHandle.findNamedView(name)
  927.                 if namedView.getView().len() > 0:
  928.                     view = namedView
  929.                     break
  930.             except:
  931.                 pass
  932.         if view is None:
  933.             return None
  934.         
  935.         return view.getView()
  936.  
  937.  
  938. ###############################################################################
  939. #### Handlers for actions generated from templates, the OS, etc            ####
  940. ###############################################################################
  941.  
  942. # Functions that are safe to call from action: URLs that do nothing
  943. # but manipulate the database.
  944. class ModelActionHandler:
  945.     
  946.     def __init__(self, backEndDelegate):
  947.         self.backEndDelegate = backEndDelegate
  948.     
  949.     def setAutoDownloadableFeed(self, feed, automatic):
  950.         obj = db.getObjectByID(int(feed))
  951.         obj.setAutoDownloadable(automatic)
  952.  
  953.     def setGetEverything(self, feed, everything):
  954.         obj = db.getObjectByID(int(feed))
  955.         obj.setGetEverything(everything == 'True')
  956.  
  957.     def setExpiration(self, feed, type, time):
  958.         obj = db.getObjectByID(int(feed))
  959.         obj.setExpiration(type, int(time))
  960.  
  961.     def setMaxNew(self, feed, maxNew):
  962.         obj = db.getObjectByID(int(feed))
  963.         obj.setMaxNew(int(maxNew))
  964.  
  965.     def startDownload(self, item):
  966.         obj = db.getObjectByID(int(item))
  967.         obj.download()
  968.  
  969.     def removeCurrentFeed(self):
  970.         currentFeed = Controller.instance.currentSelectedTab.feedID()
  971.         if currentFeed:
  972.             self.removeFeed(currentFeed)
  973.  
  974.     def removeFeed(self, feed):
  975.         obj = db.getObjectByID(int(feed))
  976.         if self.backEndDelegate.validateFeedRemoval(obj.getTitle()):
  977.             obj.remove()
  978.  
  979.     def updateCurrentFeed(self):
  980.         currentFeed = Controller.instance.currentSelectedTab.feedID()
  981.         if currentFeed:
  982.             self.updateFeed(currentFeed)
  983.  
  984.     def updateFeed(self, feed):
  985.         obj = db.getObjectByID(int(feed))
  986.         thread = threading.Thread(target=obj.update, name="updateFeed")
  987.         thread.setDaemon(False)
  988.         thread.start()
  989.  
  990.     def updateAllFeeds(self):
  991.         # We might want to limit the number of simultaneous threads but for
  992.         # now, this naive and simple implementation will do the trick.
  993.         for f in globalViewList['feeds']:
  994.             thread = threading.Thread(target=f.update, name="updateAllFeeds")
  995.             thread.setDaemon(False)
  996.             thread.start()
  997.  
  998.     def copyCurrentFeedURL(self):
  999.         currentFeed = Controller.instance.currentSelectedTab.feedID()
  1000.         if currentFeed:
  1001.             self.copyFeedURL(currentFeed)
  1002.  
  1003.     def copyFeedURL(self, feed):
  1004.         obj = db.getObjectByID(int(feed))
  1005.         url = obj.getURL()
  1006.         self.backEndDelegate.copyTextToClipboard(url)
  1007.  
  1008.     def markFeedViewed(self, feed):
  1009.         try:
  1010.             obj = db.getObjectByID(int(feed))
  1011.             obj.markAsViewed()
  1012.         except database.ObjectNotFoundError:
  1013.             pass
  1014.  
  1015.     def expireItem(self, item):
  1016.         obj = db.getObjectByID(int(item))
  1017.         obj.expire()
  1018.  
  1019.     def keepItem(self, item):
  1020.         obj = db.getObjectByID(int(item))
  1021.         obj.setKeep(True)
  1022.  
  1023.     def setRunAtStartup(self, value):
  1024.         value = (value == "1")
  1025.         self.backEndDelegate.setRunAtStartup(value)
  1026.  
  1027.     def setCheckEvery(self, value):
  1028.         value = int(value)
  1029.         config.set(config.CHECK_CHANNELS_EVERY_X_MN,value)
  1030.  
  1031.     def setLimitUpstream(self, value):
  1032.         value = (value == "1")
  1033.         config.set(config.LIMIT_UPSTREAM,value)
  1034.  
  1035.     def setMaxUpstream(self, value):
  1036.         value = int(value)
  1037.         config.set(config.UPSTREAM_LIMIT_IN_KBS,value)
  1038.  
  1039.     def setPreserveDiskSpace(self, value):
  1040.         value = (value == "1")
  1041.         config.set(config.PRESERVE_DISK_SPACE,value)
  1042.  
  1043.     def setMinDiskSpace(self, value):
  1044.         value = int(value)
  1045.         config.set(config.PRESERVE_X_GB_FREE,value)
  1046.  
  1047.     def setDefaultExpiration(self, value):
  1048.         value = int(value)
  1049.         config.set(config.EXPIRE_AFTER_X_DAYS,value)
  1050.  
  1051.     def videoBombExternally(self, item):
  1052.         obj = db.getObjectByID(int(item))
  1053.         paramList = {}
  1054.         paramList["title"] = obj.getTitle()
  1055.         paramList["info_url"] = obj.getLink()
  1056.         paramList["hookup_url"] = obj.getPaymentLink()
  1057.         try:
  1058.             rss_url = obj.getFeed().getURL()
  1059.             if (not rss_url.startswith('dtv:')):
  1060.                 paramList["rss_url"] = rss_url
  1061.         except:
  1062.             pass
  1063.         thumb_url = obj.getThumbnail()
  1064.         if (not thumb_url.startswith('resource:')):
  1065.             paramList["thumb_url"] = thumb_url
  1066.  
  1067.         # FIXME: add "explicit" and "tags" parameters when we get them in item
  1068.  
  1069.         paramString = ""
  1070.         glue = '?'
  1071.        
  1072.         # This should be first, since it's most important.
  1073.         url = obj.getURL()
  1074.         if (not url.startswith('file:')):
  1075.             paramString = "?url=%s" % xhtmltools.urlencode(url)
  1076.             glue = '&'
  1077.  
  1078.         for key in paramList.keys():
  1079.             if len(paramList[key]) > 0:
  1080.                 paramString = "%s%s%s=%s" % (paramString, glue, key, xhtmltools.urlencode(paramList[key]))
  1081.                 glue = '&'
  1082.  
  1083.         # This should be last, so that if it's extra long it 
  1084.         # cut off all the other parameters
  1085.         description = obj.getDescription()
  1086.         if len(description) > 0:
  1087.             paramString = "%s%sdescription=%s" % (paramString, glue,  xhtmltools.urlencode(description))
  1088.         url = config.get(config.VIDEOBOMB_URL) + paramString
  1089.         self.backEndDelegate.openExternalURL(url)
  1090.  
  1091. # Test shim for test* functions on GUIActionHandler
  1092. class printResultThread(threading.Thread):
  1093.  
  1094.     def __init__(self, format, func):
  1095.         self.format = format
  1096.         self.func = func
  1097.         threading.Thread.__init__(self)
  1098.  
  1099.     def run(self):
  1100.         print (self.format % (self.func(), ))
  1101.  
  1102. # Functions that are safe to call from action: URLs that can change
  1103. # the GUI presentation (and may or may not manipulate the database.)
  1104. class GUIActionHandler:
  1105.  
  1106.     def __init__(self, controller):
  1107.         self.controller = controller
  1108.  
  1109.     def selectTab(self, id, templateNameHint = None):
  1110.         try:
  1111.             cur = self.controller.checkTabByID(id)
  1112.         except: # That tab doesn't exist anymore! Give up.
  1113.             print "Tab %s doesn't exist! Cannot select it." % str(id)
  1114.             return
  1115.  
  1116.         # Figure out what happened
  1117.         oldSelected = self.controller.currentSelectedTab
  1118.         newSelected = cur
  1119.  
  1120.         # Handle reselection action (checkSelectedTab won't; it doesn't
  1121.         # see a difference)
  1122.         if oldSelected and oldSelected.id == newSelected.id:
  1123.             newSelected.start(self.controller.frame, templateNameHint)
  1124.  
  1125.         # Handle case where a different tab was clicked
  1126.         self.controller.checkSelectedTab(templateNameHint)
  1127.  
  1128.     # NEEDS: name should change to addAndSelectFeed; then we should create
  1129.     # a non-GUI addFeed to match removeFeed. (requires template updates)
  1130.  
  1131.     def addFeed(self, url, showTemplate = None, selected = '1'):
  1132.         url = feed.normalizeFeedURL(url)
  1133.         db.beginUpdate()
  1134.         try:
  1135.             feedView = globalViewList['feeds'].filterWithIndex(globalIndexList['feedsByURL'],url)
  1136.             exists = feedView.len() > 0
  1137.  
  1138.             if not exists:
  1139.                 myFeed = feed.Feed(url)
  1140.             else:
  1141.                 feedView.resetCursor()
  1142.                 myFeed = feedView.getNext()
  1143.                 # At this point, the addition is guaranteed to be reflected
  1144.                 # in the tab list.
  1145.  
  1146.             globalViewList['feeds'].removeView(feedView)
  1147.  
  1148.             if selected == '1':
  1149.                 self.controller.checkTabByObjID(myFeed.getID())
  1150.                 self.controller.checkSelectedTab(showTemplate)
  1151.  
  1152.         finally:
  1153.             db.endUpdate()
  1154.  
  1155.     # NEEDS: factor out common code with addFeed
  1156.     def selectFeed(self, url):
  1157.         url = feed.normalizeFeedURL(url)
  1158.         db.beginUpdate()
  1159.         try:
  1160.             # Find the feed
  1161.             feedView = globalViewList['feeds'].filterWithIndex(globalIndexList['feedsByURL'],url)
  1162.             exists = feedView.len() > 0
  1163.             if not exists:
  1164.                 print "selectFeed: no such feed: %s" % url
  1165.                 return
  1166.             feedView.resetCursor()
  1167.             myFeed = feedView.getNext()
  1168.             globalViewList['feeds'].removeView(feedView)
  1169.  
  1170.             # Select it
  1171.             self.controller.checkTabByObjID(myFeed.getID())
  1172.             self.controller.checkSelectedTab()
  1173.  
  1174.         finally:
  1175.             db.endUpdate()
  1176.  
  1177.     # Following for testing/debugging
  1178.  
  1179.     def showHelp(self):
  1180.         # FIXME don't hardcode this URL
  1181.         self.controller.getBackendDelegate().openExternalURL('http://www.getdemocracy.com/help')
  1182.  
  1183.     def testGetHTTPAuth(self, **args):
  1184.         printResultThread("testGetHTTPAuth: got %s", lambda: self.controller.getBackendDelegate().getHTTPAuth(**args)).start()
  1185.  
  1186.     def testIsScrapeAllowed(self, url):
  1187.         printResultThread("testIsScrapeAllowed: got %s", lambda: self.controller.getBackendDelegate().isScrapeAllowed(url)).start()
  1188.  
  1189. # Functions that are safe to call from action: URLs that change state
  1190. # specific to a particular instantiation of a template, and so have to
  1191. # be scoped to a particular HTML display widget.
  1192. class TemplateActionHandler:
  1193.     
  1194.     def __init__(self, controller, display, templateHandle):
  1195.         self.controller = controller
  1196.         self.display = display
  1197.         self.templateHandle = templateHandle
  1198.  
  1199.     def switchTemplate(self, name, baseURL=None):
  1200.         # Graphically indicate that we're not at the home
  1201.         # template anymore
  1202.         self.controller.setTabListActive(False)
  1203.  
  1204.         self.templateHandle.unlinkTemplate()
  1205.         # Switch to new template. It get the same variable
  1206.         # dictionary as we have.
  1207.         # NEEDS: currently we hardcode the display area. This means
  1208.         # that these links always affect the right-hand 'content'
  1209.         # area, even if they are loaded from the left-hand 'tab'
  1210.         # area. Actually this whole invocation is pretty hacky.
  1211.         template = TemplateDisplay(name, self.display.templateData, self.controller, existingView = "sharedView", frameHint=self.controller.frame, areaHint=self.controller.frame.mainDisplay, baseURL=baseURL)
  1212.         self.controller.frame.selectDisplay(template, self.controller.frame.mainDisplay)
  1213.  
  1214.     def doneWithIntro(self):
  1215.         # Find the guide
  1216.         guide = None
  1217.         for obj in globalViewList['guide']:
  1218.             guide = obj
  1219.         assert guide is not None
  1220.  
  1221.         guide.setSawIntro()
  1222.         self.goToGuide()
  1223.  
  1224.     def goToGuide(self):
  1225.         # Find the guide
  1226.         guide = None
  1227.         for obj in globalViewList['guide']:
  1228.             guide = obj
  1229.         assert guide is not None
  1230.  
  1231.         # Does the Guide want to implement itself as a redirection to
  1232.         # a URL?
  1233.         (mode, location) = guide.getLocation()
  1234.  
  1235.         if mode == 'template':
  1236.             self.switchTemplate(location, baseURL=config.get(config.CHANNEL_GUIDE_URL))
  1237.         elif mode == 'url':
  1238.             self.controller.frame.selectURL(location, \
  1239.                                             self.controller.frame.mainDisplay)
  1240.         else:
  1241.             assert False, "Invalid guide load mode '%s'" % mode
  1242.  
  1243.     def setViewFilter(self, viewName, fieldKey, functionKey, parameter, invert):
  1244.         #print "set filter: view %s field %s func %s param %s invert %s" % (viewName, fieldKey, functionKey, parameter, invert)
  1245.         if viewName != "undefined":
  1246.             invert = stringToBoolean(invert)
  1247.             namedView = self.templateHandle.findNamedView(viewName)
  1248.             namedView.setFilter(fieldKey, functionKey, parameter, invert)
  1249.  
  1250.     def setViewSort(self, viewName, fieldKey, functionKey, reverse="false"):
  1251.         #print "set sort: view %s field %s func %s reverse %s" % (viewName, fieldKey, functionKey, reverse)
  1252.         reverse = stringToBoolean(reverse)
  1253.         namedView = self.templateHandle.findNamedView(viewName)
  1254.         namedView.setSort(fieldKey, functionKey, reverse)
  1255.  
  1256.     def playViewNamed(self, viewName, firstItemId):
  1257.         # Find the database view that we're supposed to be
  1258.         # playing; take out items that aren't playable video
  1259.         # clips and put it in the format the frontend expects.
  1260.         namedView = self.templateHandle.findNamedView(viewName)
  1261.         view = namedView.getView()
  1262.         self.playView(view, firstItemId)
  1263.  
  1264.     def playView(self, view, firstItemId):
  1265.         self.controller.playbackController.configure(view, firstItemId)
  1266.         self.controller.playbackController.enterPlayback()
  1267.  
  1268.     def playItemExternally(self, itemID):
  1269.         self.controller.playbackController.playItemExternally(itemID)
  1270.         
  1271.     def skipItem(self, itemID):
  1272.         self.controller.playbackController.skip(1)
  1273.     
  1274.     def updateLastSearchEngine(self, engine):
  1275.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  1276.         searchFeed.lastEngine = engine
  1277.     
  1278.     def updateLastSearchQuery(self, query):
  1279.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  1280.         searchFeed.lastQuery = query
  1281.         
  1282.     def performSearch(self, engine, query):
  1283.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  1284.         searchFeed.preserveDownloads(searchDownloadsFeed)
  1285.         searchFeed.lookup(engine, query)
  1286.  
  1287.     def resetSearch(self):
  1288.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  1289.         searchFeed.preserveDownloads(searchDownloadsFeed)
  1290.         searchFeed.reset()
  1291.         
  1292.     def __getSearchFeeds(self):
  1293.         searchFeed = self.controller.getGlobalFeed('dtv:search')
  1294.         assert searchFeed is not None
  1295.         searchDownloadsFeed = Controller.instance.getGlobalFeed('dtv:searchDownloads')
  1296.         assert searchDownloadsFeed is not None
  1297.         return (searchFeed, searchDownloadsFeed)
  1298.  
  1299.     # The Windows XUL port can send a setVolume or setVideoProgress at
  1300.     # any time, even when there's no video display around. We can just
  1301.     # ignore it
  1302.     def setVolume(self, level):
  1303.         pass
  1304.     def setVideoProgress(self, pos):
  1305.         pass
  1306.  
  1307. # Helper: liberally interpret the provided string as a boolean
  1308. def stringToBoolean(string):
  1309.     if string == "" or string == "0" or string == "false":
  1310.         return False
  1311.     else:
  1312.         return True
  1313.  
  1314. ###############################################################################
  1315. #### Tabs                                                                  ####
  1316. ###############################################################################
  1317.  
  1318. class Tab:
  1319.     idCounter = 0
  1320.  
  1321.     def __init__(self, tabTemplateBase, tabData, contentsTemplate, contentsData, sortKey, obj, controller):
  1322.         self.tabTemplateBase = tabTemplateBase
  1323.         self.tabData = tabData
  1324.         self.contentsTemplate = contentsTemplate
  1325.         self.contentsData = contentsData
  1326.         self.sortKey = sortKey
  1327.         self.controller = controller
  1328.         self.display = None
  1329.         self.id = "tab%d" % Tab.idCounter
  1330.         Tab.idCounter += 1
  1331.         self.obj = obj
  1332.  
  1333.     def start(self, frame, templateNameHint):
  1334.         self.controller.setTabListActive(True)
  1335.         self.display = TemplateDisplay(templateNameHint or self.contentsTemplate, self.contentsData, self.controller, existingView="sharedView", frameHint=frame, areaHint=frame.mainDisplay)
  1336.         frame.selectDisplay(self.display, frame.mainDisplay)
  1337.  
  1338.     def markup(self):
  1339.         """Get HTML giving the visual appearance of the tab. 'state' is
  1340.         one of 'selected' (tab is currently selected), 'normal' (tab is
  1341.         not selected), or 'selected-inactive' (tab is selected but
  1342.         setTabListActive was called with a false value on the MainFrame
  1343.         for which the tab is being rendered.) The HTML should be returned
  1344.         as a xml.dom.minidom element or document fragment."""
  1345.         state = self.controller.getTabState(self.id)
  1346.         file = "%s-%s" % (self.tabTemplateBase, state)
  1347.         return template.fillStaticTemplate(file, self.tabData)
  1348.  
  1349.     def redraw(self):
  1350.         # Force a redraw by sending a change notification on the underlying
  1351.         # DB object.
  1352.         self.obj.beginChange()
  1353.         self.obj.endChange()
  1354.  
  1355.     def isFeed(self):
  1356.         """True if this Tab represents a Feed."""
  1357.         return isinstance(self.obj, feed.Feed)
  1358.  
  1359.     def feedURL(self):
  1360.         """If this Tab represents a Feed, the feed's URL. Otherwise None."""
  1361.         if self.isFeed():
  1362.             return self.obj.getURL()
  1363.         else:
  1364.             return None
  1365.  
  1366.     def feedID(self):
  1367.         """If this Tab represents a Feed, the feed's ID. Otherwise None."""
  1368.         if self.isFeed():
  1369.             return self.obj.getID()
  1370.         else:
  1371.             return None
  1372.  
  1373.     def onDeselected(self, frame):
  1374.         self.display.onDeselect(frame)
  1375.  
  1376. # Database object representing a static (non-feed-associated) tab.
  1377. class StaticTab(database.DDBObject):
  1378.     def __init__(self, tabTemplateBase, contentsTemplate, order):
  1379.         self.tabTemplateBase = tabTemplateBase
  1380.         self.contentsTemplate = contentsTemplate
  1381.         self.order = order
  1382.         database.DDBObject.__init__(self)
  1383.  
  1384. # Remove all static tabs from the database
  1385. def removeStaticTabs():
  1386.     db.beginUpdate()
  1387.     try:
  1388.         for obj in globalViewList['staticTabs']:
  1389.             obj.remove()
  1390.     finally:
  1391.         db.endUpdate()
  1392.  
  1393. # Reload the StaticTabs in the database from the statictabs.xml resource file.
  1394. def reloadStaticTabs():
  1395.     db.beginUpdate()
  1396.     try:
  1397.         # Wipe all of the StaticTabs currently in the database.
  1398.         removeStaticTabs()
  1399.  
  1400.         # Load them anew from the resource file.
  1401.         # NEEDS: maybe better error reporting?
  1402.         document = parse(resource.path('statictabs.xml'))
  1403.         for n in document.getElementsByTagName('statictab'):
  1404.             tabTemplateBase = n.getAttribute('tabtemplatebase')
  1405.             contentsTemplate = n.getAttribute('contentstemplate')
  1406.             order = int(n.getAttribute('order'))
  1407.             StaticTab(tabTemplateBase, contentsTemplate, order)
  1408.     finally:
  1409.         db.endUpdate()
  1410.  
  1411. # Return True if a tab should be shown for obj in the frontend. The filter
  1412. # used on the database to get the list of tabs.
  1413. def mappableToTab(obj):
  1414.     return isinstance(obj, StaticTab) or (isinstance(obj, feed.Feed) and
  1415.                                           obj.isVisible())
  1416.  
  1417. # Generate a function that, given an object for which mappableToTab
  1418. # returns true, return a Tab instance -- mapping a model object into
  1419. # a UI objet that can be rendered and selected.
  1420. #
  1421. # By 'generate a function', we mean that you give makeMapToTabFunction
  1422. # the global data that you want to always be available in both the tab
  1423. # templates and the contents page template, and it returns a function
  1424. # that maps objects to tabs such that that request is satisified.
  1425. def makeMapToTabFunction(globalTemplateData, controller):
  1426.     class MapToTab:
  1427.         def __init__(self, globalTemplateData):
  1428.             self.globalTemplateData = globalTemplateData
  1429.  
  1430.         def mapToTab(self,obj):
  1431.             data = {'global': self.globalTemplateData};
  1432.             if isinstance(obj, StaticTab):
  1433.                 if obj.contentsTemplate == 'search':
  1434.                     data['feed'] = Controller.instance.getGlobalFeed('dtv:search')
  1435.                 return Tab(obj.tabTemplateBase, data, obj.contentsTemplate, data, [obj.order], obj, controller)
  1436.             elif isinstance(obj, feed.Feed):
  1437.                 data['feed'] = obj
  1438.                 # Change this to sort feeds on a different value
  1439.                 sortKey = obj.getTitle().lower()
  1440.                 return Tab('feedtab', data, 'channel', data, [100, sortKey], obj, controller)
  1441.             elif isinstance(obj, folder.Folder):
  1442.                 data['folder'] = obj
  1443.                 sortKey = obj.getTitle()
  1444.                 return Tab('foldertab',data,'folder',data,[500,sortKey],obj,controller)
  1445.             else:
  1446.                 assert(0) # NEEDS: clean up (signal internal error)
  1447.  
  1448.     return MapToTab(globalTemplateData).mapToTab
  1449.  
  1450. # The sort function used to order tabs in the tab list: just use the
  1451. # sort keys provided when mapToTab created the Tabs. These can be
  1452. # lists, which are tested left-to-right in the way you'd
  1453. # expect. Generally, the way this is used is that static tabs are
  1454. # assigned a numeric priority, and get a single-element list with that
  1455. # number as their sort key; feeds get a list with '100' in the first
  1456. # position, and a value that determines the order of the feeds in the
  1457. # second position. This way all of the feeds are together, and the
  1458. # static tabs can be positioned around them.
  1459. def sortTabs(x, y):
  1460.     if x.sortKey < y.sortKey:
  1461.         return -1
  1462.     elif x.sortKey > y.sortKey:
  1463.         return 1
  1464.     return 0
  1465.  
  1466.  
  1467. ###############################################################################
  1468. #### Playlist & Video clips                                                ####
  1469. ###############################################################################
  1470.  
  1471. class Playlist:
  1472.     
  1473.     def __init__(self, view, firstItemId):
  1474.         self.initialView = view
  1475.         self.filteredView = self.initialView.filter(mappableToPlaylistItem)
  1476.         self.view = self.filteredView.map(mapToPlaylistItem)
  1477.  
  1478.         # Move the cursor to the requested item; if there's no
  1479.         # such item in the view, move the cursor to the first
  1480.         # item
  1481.         self.view.beginRead()
  1482.         try:
  1483.             self.view.resetCursor()
  1484.             while True:
  1485.                 cur = self.view.getNext()
  1486.                 if cur == None:
  1487.                     # Item not found in view. Put cursor at the first
  1488.                     # item, if any.
  1489.                     self.view.resetCursor()
  1490.                     self.view.getNext()
  1491.                     break
  1492.                 if str(cur.getID()) == firstItemId:
  1493.                     # The cursor is now on the requested item.
  1494.                     break
  1495.         finally:
  1496.             self.view.endRead()
  1497.  
  1498.     def reset(self):
  1499.         self.initialView.removeView(self.filteredView)
  1500.         self.initialView = None
  1501.         self.filteredView = None
  1502.         self.view = None
  1503.  
  1504.     def cur(self):
  1505.         return self.itemMarkedAsViewed(self.view.cur())
  1506.  
  1507.     def getNext(self):
  1508.         return self.itemMarkedAsViewed(self.view.getNext())
  1509.         
  1510.     def getPrev(self):
  1511.         return self.itemMarkedAsViewed(self.view.getPrev())
  1512.  
  1513.     def itemMarkedAsViewed(self, anItem):
  1514.         if anItem is not None:
  1515.             anItem.onViewed()
  1516.         return anItem
  1517.  
  1518. class PlaylistItemFromItem (frontend.PlaylistItem):
  1519.  
  1520.     def __init__(self, anItem):
  1521.         self.item = anItem
  1522.  
  1523.     def getTitle(self):
  1524.         return self.item.getTitle()
  1525.  
  1526.     def getPath(self):
  1527.         return self.item.getFilename()
  1528.  
  1529.     def getLength(self):
  1530.         # NEEDS
  1531.         return 42.42
  1532.  
  1533.     def onViewed(self):
  1534.         self.item.markItemSeen()
  1535.  
  1536.     # Return the ID that is used by a template to indicate this item 
  1537.     def getID(self):
  1538.         return self.item.getID()
  1539.  
  1540.     # Return a dictionary containing info to be injected in a template
  1541.     def getInfoMap(self):
  1542.         return dict(this=self.item, filter=globalFilterList)
  1543.  
  1544.     def __getattr__(self, attr):
  1545.         return getattr(self.item, attr)
  1546.  
  1547. def mappableToPlaylistItem(obj):
  1548.     if not isinstance(obj, item.Item):
  1549.         return False
  1550.  
  1551.     return (obj.getState() == "finished" or obj.getState() == "uploading" or
  1552.             obj.getState() == "watched" or obj.getState() == "saved")
  1553.  
  1554. def mapToPlaylistItem(obj):
  1555.     return PlaylistItemFromItem(obj)
  1556.  
  1557.  
  1558. ###############################################################################
  1559. #### The global set of filter and sort functions accessible from templates ####
  1560. ###############################################################################
  1561.  
  1562. def compare(x, y):
  1563.     if x < y:
  1564.         return -1
  1565.     if x > y:
  1566.         return 1
  1567.     return 0
  1568.  
  1569. def itemSort(x,y):
  1570.     if x.getReleaseDateObj() > y.getReleaseDateObj():
  1571.         return -1
  1572.     elif x.getReleaseDateObj() < y.getReleaseDateObj():
  1573.         return 1
  1574.     elif x.getLinkNumber() > y.getLinkNumber():
  1575.         return -1
  1576.     elif x.getLinkNumber() < y.getLinkNumber():
  1577.         return 1
  1578.     elif x.getID() > y.getID():
  1579.         return -1
  1580.     elif x.getID() < y.getID():
  1581.         return 1
  1582.     else:
  1583.         return 0
  1584.  
  1585. def alphabeticalSort(x,y):
  1586.     if x.getTitle() < y.getTitle():
  1587.         return -1
  1588.     elif x.getTitle() > y.getTitle():
  1589.         return 1
  1590.     elif x.getDescription() < y.getDescription():
  1591.         return -1
  1592.     elif x.getDescription() > y.getDescription():
  1593.         return 1
  1594.     else:
  1595.         return 0
  1596.  
  1597. def downloadStartedSort(x,y):
  1598.     if x.getTitle() < y.getTitle():
  1599.         return -1
  1600.     elif x.getTitle() > y.getTitle():
  1601.         return 1
  1602.     elif x.getDescription() < y.getDescription():
  1603.         return -1
  1604.     elif x.getDescription() > y.getDescription():
  1605.         return 1
  1606.     else:
  1607.         return 0
  1608.  
  1609. globalSortList = {
  1610.     'item': itemSort,
  1611.     'alphabetical': alphabeticalSort,
  1612.     'tab': sortTabs,
  1613.     'downloadStarted': downloadStartedSort,
  1614.     'text': (lambda x, y: compare(str(x), str(y))),
  1615.     'number': (lambda x, y: compare(float(x), float(y))),
  1616. }
  1617.  
  1618. def filterClass(obj, parameter):
  1619.     if type(obj) != types.InstanceType:
  1620.         return False
  1621.  
  1622.     # Pull off any package name
  1623.     name = str(obj.__class__)
  1624.     match = re.compile(r"\.([^.]*)$").search(name)
  1625.     if match:
  1626.         name = match.group(1)
  1627.  
  1628.     return name == parameter
  1629.  
  1630. def filterHasKey(obj,parameter):
  1631.     try:
  1632.         obj[parameter]
  1633.     except KeyError:
  1634.         return False
  1635.     return True
  1636.  
  1637. # FIXME: All of these functions have a big hack to support two
  1638. #        parameters instead of one. It's ugly. We should fix this to
  1639. #        support multiple parameters
  1640. def unviewedItems(obj, param):
  1641.     params = param.split('|',1)
  1642.     
  1643.     unviewed = (str(obj.feed.getID()) == params[0] and 
  1644.                 not obj.getViewed())
  1645.     if len(params) > 1:
  1646.         unviewed= (unviewed and 
  1647.                    (str(params[1]).lower() in obj.getTitle().lower() or
  1648.                     str(params[1]).lower() in obj.getDescription().lower()))
  1649.     return unviewed
  1650.  
  1651. def viewedItems(obj, param):
  1652.     params = param.split('|',1)
  1653.     
  1654.     viewed = (str(obj.feed.getID()) == params[0] and 
  1655.               obj.getViewed())
  1656.     if len(params) > 1:
  1657.         viewed= (viewed and 
  1658.                  (str(params[1]).lower() in obj.getTitle().lower() or
  1659.                   str(params[1]).lower() in obj.getDescription().lower()))
  1660.     return viewed
  1661.  
  1662. def undownloadedItems(obj,param):
  1663.     params = param.split('|',1)
  1664.     
  1665.     undled = (str(obj.feed.getID()) == params[0] and 
  1666.               (obj.getState() == 'stopped' or
  1667.                obj.getState() == 'downloading'))
  1668.     if len(params) > 1:
  1669.         undled = (undled and 
  1670.                   (str(params[1]).lower() in obj.getTitle().lower() or
  1671.                    str(params[1]).lower() in obj.getDescription().lower()))
  1672.     return undled
  1673.  
  1674. def downloadingItems(obj, param):
  1675.     params = param.split('|',1)
  1676.     
  1677.     old = (str(obj.feed.getID()) == params[0] and 
  1678.            obj.getState() == 'downloading')
  1679.     if len(params) > 1:
  1680.         old = (old and 
  1681.                (str(params[1]).lower() in obj.getTitle().lower() or
  1682.                 str(params[1]).lower() in obj.getDescription().lower()))
  1683.     return old
  1684.  
  1685. def unwatchedItems(obj, param):
  1686.     params = param.split('|',1)
  1687.     unwatched = True
  1688.     if params[0] != '':
  1689.         unwatched = (str(obj.feed.getID()) == params[0])
  1690.     if len(params) > 1:
  1691.         unwatched = (unwatched and 
  1692.                      (str(params[1]).lower() in obj.getTitle().lower() or
  1693.                       str(params[1]).lower() in obj.getDescription().lower()))
  1694.     unwatched = (unwatched and 
  1695.                  ((obj.getState() == 'finished' or
  1696.                    obj.getState() == 'uploading')))
  1697.     return unwatched
  1698.  
  1699. def expiringItems(obj, param):
  1700.     params = param.split('|',1)
  1701.     expiring = True
  1702.     if params[0] != '':
  1703.         expiring = (str(obj.feed.getID()) == params[0])
  1704.     if len(params) > 1:
  1705.         expiring = (expiring and 
  1706.                 (str(params[1]).lower() in obj.getTitle().lower() or
  1707.                  str(params[1]).lower() in obj.getDescription().lower()))
  1708.     expiring = (expiring and (obj.getState() == 'watched'))
  1709.     return expiring
  1710.  
  1711. def feedItems(obj, param):
  1712.     params = param.split('|',1)
  1713.     
  1714.     dled = (str(obj.feed.getID()) == params[0])
  1715.     if len(params) > 1:
  1716.         dled = (dled and 
  1717.                 (str(params[1]).lower() in obj.getTitle().lower() or
  1718.                  str(params[1]).lower() in obj.getDescription().lower()))
  1719.     return dled
  1720.  
  1721. def recentItems(obj, param):
  1722.     #FIXME make this look at the feed's time until expiration
  1723.     params = param.split('|',1)
  1724.     
  1725.     recent = (str(obj.feed.getID()) == params[0] and 
  1726.               ((obj.getState() == 'finished' or
  1727.                 obj.getState() == 'uploading' or
  1728.                 obj.getState() == 'watched')))
  1729.     if len(params) > 1:
  1730.         recent = (recent and 
  1731.                   (str(params[1]).lower() in obj.getTitle().lower() or
  1732.                    str(params[1]).lower() in obj.getDescription().lower()))
  1733.     return recent
  1734.  
  1735. def oldItems(obj, param):
  1736.     params = param.split('|',1)
  1737.     
  1738.     old = (str(obj.feed.getID()) == params[0] and 
  1739.            obj.getState() == 'saved')
  1740.     if len(params) > 1:
  1741.         old = (old and 
  1742.                (str(params[1]).lower() in obj.getTitle().lower() or
  1743.                 str(params[1]).lower() in obj.getDescription().lower()))
  1744.     return old
  1745.  
  1746. def watchableItems(obj, param):
  1747.     params = param.split('|',1)
  1748.  
  1749.     if len(params)>1:
  1750.         search = params[1]
  1751.     else:
  1752.         search = ''
  1753.  
  1754.     return (((len(params[0]) == 0) or str(obj.feed.getID()) == params[0]) and 
  1755.             ((obj.getState() == 'finished' or
  1756.               obj.getState() == 'uploading' or
  1757.               obj.getState() == 'watched' or
  1758.               obj.getState() == 'saved')) and
  1759.             (search.lower() in obj.getTitle().lower() or 
  1760.              search.lower() in obj.getDescription().lower()))
  1761.     
  1762. def allRecentItems(obj, param):
  1763.     params = param.split('|',1)
  1764.     if len(params)>1:
  1765.         search = params[1]
  1766.     else:
  1767.         search = ''
  1768.  
  1769.     return ((obj.getState() == 'finished' or obj.getState() == 'uploading' or
  1770.             obj.getState() == 'watched') and 
  1771.             (search.lower() in obj.getTitle().lower() or 
  1772.              search.lower() in obj.getDescription().lower()))
  1773.  
  1774. def allDownloadingItems(obj, param):
  1775.     params = param.split('|',1)
  1776.     if len(params)>1:
  1777.         search = params[1]
  1778.     else:
  1779.         search = ''
  1780.  
  1781.     return (obj.getState() == 'downloading' and
  1782.             (search.lower() in obj.getTitle().lower() or 
  1783.              search.lower() in obj.getDescription().lower()))
  1784.  
  1785. globalFilterList = {
  1786.     'substring': (lambda x, y: str(y) in str(x)),
  1787.     'boolean': (lambda x, y: x),
  1788.  
  1789.     'unviewedItems': unviewedItems,
  1790.     'viewedItems': viewedItems,
  1791.  
  1792.     'feedItems' : feedItems,
  1793.     'recentItems': recentItems,
  1794.     'allRecentItems': allRecentItems,
  1795.     'oldItems': oldItems,
  1796.     'watchableItems': watchableItems,
  1797.     'downloadingItems': downloadingItems,
  1798.     'unwatchedItems': unwatchedItems,
  1799.     'expiringItems': expiringItems,
  1800.     'undownloadedItems':  undownloadedItems,
  1801.     'allDownloadingItems': allDownloadingItems,
  1802.     
  1803.     'class': filterClass,
  1804.     'all': (lambda x, y: True),
  1805.     'hasKey':  filterHasKey,
  1806.     'equal':(lambda x, y: str(x) == str(y)),
  1807.     'feedID': (lambda x, y: str(x.getFeedID()) == str(y))
  1808. }
  1809.  
  1810. globalViewList = {}  # filled in below with indexed view
  1811.  
  1812. # Returns the class of the object, aggregating all Item subtypes under Item
  1813. def getClassForFilter(x):
  1814.     if isinstance(x,item.Item):
  1815.         return item.Item
  1816.     else:
  1817.         return x.__class__
  1818.  
  1819. globalIndexList = {
  1820.     'itemsByFeed': lambda x:str(x.getFeed().getID()),
  1821.     'feedsByURL': lambda x:str(x.getURL()),
  1822.     'downloadsByDLID': lambda x:str(x.dlid),
  1823.     'class': getClassForFilter
  1824. }
  1825.  
  1826. db.createIndex(globalIndexList['class'])
  1827. globalViewList['items'] = db.filterWithIndex(globalIndexList['class'],item.Item)
  1828. globalViewList['feeds'] = db.filterWithIndex(globalIndexList['class'],feed.Feed)
  1829. globalViewList['httpauths'] = db.filterWithIndex(globalIndexList['class'],downloader.HTTPAuthPassword)
  1830. globalViewList['staticTabs'] = db.filterWithIndex(globalIndexList['class'],StaticTab)
  1831. globalViewList['guide'] =  db.filterWithIndex(globalIndexList['class'],guide.ChannelGuide)
  1832. globalViewList['availableItems'] = globalViewList['items'].filter(lambda x:x.getState() == 'finished' or x.getState() == 'uploading')
  1833. globalViewList['downloadingItems'] = globalViewList['items'].filter(lambda x:x.getState() == 'downloading')
  1834. globalViewList['remoteDownloads'] = db.filterWithIndex(globalIndexList['class'],downloader.RemoteDownloader)
  1835.  
  1836. globalViewList['remoteDownloads'].createIndex(globalIndexList['downloadsByDLID'])
  1837. globalViewList['items'].createIndex(globalIndexList['itemsByFeed'])
  1838. globalViewList['feeds'].createIndex(globalIndexList['feedsByURL'])
  1839.